package cn.edu.hit.core.db;


import java.util.*;

import static java.util.Arrays.asList;

public final class Filters {

    private Filters() {
    }

    public static <TItem> Filter<TItem> eq(final TItem value) {
        return eq("_id", value);
    }

    /**
     * Creates a filter that matches all documents where the value of the field name equals the specified value. Note that this doesn't
     * actually generate a $eq operator, as the query language doesn't require it.
     *
     * @param fieldName the field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/eq $eq
     */
    public static <TItem> Filter<TItem> eq(final String fieldName, final TItem value) {
        return new SimpleFilter<>(fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the field name does not equal the specified value.
     *
     * @param fieldName the field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/ne $ne
     */
    public static <TItem> OperatorFilter<TItem> ne(final String fieldName, final TItem value) {
        return new OperatorFilter<>("$ne", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is greater than the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/gt $gt
     */
    public static <TItem> Filter gt(final String fieldName, final TItem value) {
        return new OperatorFilter<>("$gt", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is less than the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/lt $lt
     */
    public static <TItem> Filter lt(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$lt", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is greater than or equal to the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/gte $gte
     */
    public static <TItem> Filter gte(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$gte", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is less than or equal to the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/lte $lte
     */
    public static <TItem> Filter lte(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$lte", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of a field equals any value in the list of specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/in $in
     */
    public static <TItem> Filter in(final String fieldName, final TItem... values) {
        return in(fieldName, asList(values));
    }

    /**
     * Creates a filter that matches all documents where the value of a field equals any value in the list of specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/in $in
     */
    public static <TItem> Filter in(final String fieldName, final List<TItem> values) {
        return new OperatorFilter<>(fieldName, "$in", values);
    }

    /**
     * Creates a filter that matches all documents where the value of a field does not equal any of the specified values or does not exist.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nin $nin
     */
    public static <TItem> Filter nin(final String fieldName, final TItem... values) {
        return nin(fieldName, asList(values));
    }

    /**
     * Creates a filter that matches all documents where the value of a field does not equal any of the specified values or does not exist.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nin $nin
     */
    public static <TItem> Filter nin(final String fieldName, final Iterable<TItem> values) {
        return new OperatorFilter<>(fieldName, "$nin", values);
    }

    /**
     * Creates a filter that performs a logical AND of the provided list of filters.  Note that this will only generate a "$and"
     * operator if absolutely necessary, as the query language implicity ands together all the keys.  In other words, a query expression
     * like:
     *
     * <blockquote><pre>
     *    and(eq("x", 1), lt("y", 3))
     * </pre></blockquote>
     * <p>
     * will generate a MongoDB query like:
     * <blockquote><pre>
     *    {x : 1, y : {$lt : 3}}
     * </pre></blockquote>
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/and $and
     */
    public static Filter and(final List<Filter> filters) {
        return new AndOrNorFilter(AndOrNorFilter.Operator.AND, filters);
    }

    /**
     * Creates a filter that performs a logical AND of the provided list of filters.  Note that this will only generate a "$and"
     * operator if absolutely necessary, as the query language implicity ands together all the keys.  In other words, a query expression
     * like:
     *
     * <blockquote><pre>
     *    and(eq("x", 1), lt("y", 3))
     * </pre></blockquote>
     * <p>
     * will generate a MongoDB query like:
     *
     * <blockquote><pre>
     *    {x : 1, y : {$lt : 3}}
     * </pre></blockquote>
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/and $and
     */
    public static Filter and(final Filter... filters) {
        return and(asList(filters));
    }

    /**
     * Creates a filter that preforms a logical OR of the provided list of filters.
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/or $or
     */
    public static Filter or(final List<Filter> filters) {
        return new AndOrNorFilter(AndOrNorFilter.Operator.OR, filters);
    }

    /**
     * Creates a filter that preforms a logical OR of the provided list of filters.
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/or $or
     */
    public static Filter or(final Filter... filters) {
        return or(asList(filters));
    }

    /**
     * Creates a filter that matches all documents that do not match the passed in filter.
     * Requires the field name to passed as part of the value passed in and lifts it to create a valid "$not" query:
     *
     * <blockquote><pre>
     *    not(eq("x", 1))
     * </pre></blockquote>
     * <p>
     * will generate a MongoDB query like:
     * <blockquote><pre>
     *    {x : $not: {$eq : 1}}
     * </pre></blockquote>
     *
     * @param filter the value
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/not $not
     */
    public static Filter not(final Filter filter) {
        return NotFilter.not(filter);
    }

    /**
     * Creates a filter that performs a logical NOR operation on all the specified filters.
     *
     * @param filters the list of values
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nor $nor
     */
    public static Filter nor(final Filter... filters) {
        return nor(asList(filters));
    }

    /**
     * Creates a filter that performs a logical NOR operation on all the specified filters.
     *
     * @param filters the list of values
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nor $nor
     */
    public static Filter nor(final List<Filter> filters) {
        return new AndOrNorFilter(AndOrNorFilter.Operator.NOR, filters);
    }

    /**
     * Creates a filter that matches all documents that contain the given field.
     *
     * @param fieldName the field name
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/exists $exists
     */
    public static Filter exists(final String fieldName) {
        return exists(fieldName, true);
    }

    /**
     * Creates a filter that matches all documents that either contain or do not contain the given field, depending on the value of the
     * exists parameter.
     *
     * @param fieldName the field name
     * @param exists    true to check for existence, false to check for absence
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/exists $exists
     */

    public static Filter exists(final String fieldName, final boolean exists) {
        return new OperatorFilter<>("$exists", fieldName, exists);
    }

    /**
     * Creates a filter that matches all documents where the value of the field is of the specified BSON type.
     *
     * @param fieldName the field name
     * @param type      the BSON type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/type $type
     */
    public static Filter type(final String fieldName, final String type) {
        return new OperatorFilter<>("$type", fieldName, type);
    }

    /**
     * Creates a filter that matches all documents where the value of a field divided by a divisor has the specified remainder (i.e. perform
     * a modulo operation to select documents).
     *
     * @param fieldName the field name
     * @param divisor   the modulus
     * @param remainder the remainder
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/mod $mod
     */
    public static Filter mod(final String fieldName, final long divisor, final long remainder) {
        return new OperatorFilter<>("$mod", fieldName, asList(divisor, remainder));
    }

    /**
     * Creates a filter that matches all documents where the value of a field is an array that contains all the specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/all $all
     */
    public static <TItem> Filter all(final String fieldName, final TItem... values) {
        return all(fieldName, asList(values));
    }

    /**
     * Creates a filter that matches all documents where the value of a field is an array that contains all the specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/all $all
     */
    public static <TItem> Filter all(final String fieldName, final Iterable<TItem> values) {
        return new OperatorFilter<>(fieldName, "$all", values);
    }

    /**
     * Creates a filter that matches all documents where the value of a field is an array of the specified size.
     *
     * @param fieldName the field name
     * @param size      the size of the array
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/size $size
     */
    public static Filter size(final String fieldName, final int size) {
        return new OperatorFilter<>("$size", fieldName, size);
    }

    /**
     * Creates a filter that matches all documents where all of the bit positions are clear in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAllClear $bitsAllClear
     * @since 3.2
     */
    public static Filter bitsAllClear(final String fieldName, final long bitmask) {
        return new OperatorFilter<>("$bitsAllClear", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents where all of the bit positions are set in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAllSet $bitsAllSet
     * @since 3.2
     */
    public static Filter bitsAllSet(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAllSet", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents where any of the bit positions are clear in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAllClear $bitsAllClear
     * @since 3.2
     */
    public static Filter bitsAnyClear(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAnyClear", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents where any of the bit positions are set in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAnySet $bitsAnySet
     * @since 3.2
     */
    public static Filter bitsAnySet(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAnySet", fieldName, bitmask);
    }

    public static <T> T notNull(String field, T value) {
        if (value == null) {
            throw new IllegalArgumentException(field + " can not be null");
        } else {
            return value;
        }
    }

    static final class SimpleFilter<BsonValue> extends Filter<BsonValue> {
        private SimpleFilter(final String fieldName, final BsonValue value) {
            super(fieldName, value);
        }

        @Override
        public String toString() {
            return operatorFilterToString(fieldName, "$eq", value);
        }
    }

    static final class OperatorFilter<TItem> extends Filter<Filter<TItem>> {
        OperatorFilter(final String operatorName, final String fieldName, final TItem value) {
            super(fieldName, new SimpleFilter<>(operatorName, value));
        }

        @Override
        public String toString() {
            return operatorFilterToString(fieldName, value.fieldName, value.value);
        }
    }

    static final class AndOrNorFilter extends Filter<List<Filter>> {
        enum Operator {
            AND("$and", "And"),
            OR("$or", "Or"),
            NOR("$nor", "Nor");

            private final String name;
            private final String toStringName;

            Operator(final String name, final String toStringName) {
                this.name = name;
                this.toStringName = toStringName;
            }
        }

        AndOrNorFilter(final Operator operator, final List<Filter> filters) {
            super(operator.name, filters);
        }

        @Override
        public String toString() {
            return Operator.valueOf(fieldName).toStringName + " Filter{"
                    + "filters=" + value
                    + '}';
        }
    }

    static class NotFilter extends Filter<Filter> {
        NotFilter(final Filter filter) {
            super("$not", filter);
        }

        public static Filter not(final Filter filter) {
            if (filter.fieldName.startsWith("$")) {
                return new NotFilter(filter);
            } else if (filter instanceof Filter) {
                return new OperatorFilter("$not", filter.fieldName, filter.value);
            }
            return new OperatorFilter("$not", filter.fieldName, new SimpleFilter<>("$eq", filter.value));
        }

        @Override
        public String toString() {
            return "Not Filter{"
                    + "filter=" + value
                    + '}';
        }
    }

    public static class Filter<E> implements Map<String, E> {
        protected final String fieldName;
        protected final E value;

        public Filter(String fieldName, E value) {
            this.fieldName = fieldName;
            this.value = value;
        }

        @Override
        public int size() {
            return 1;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean containsKey(Object key) {
            return fieldName.equals(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return this.value.equals(value);
        }

        @Override
        public E get(Object key) {
            return containsKey(key) ? value : null;
        }

        @Override
        public E put(String key, E value) {
            throw new IllegalArgumentException("[put] can not be called in a Filter");
        }

        @Override
        public E remove(Object key) {
            throw new IllegalArgumentException("[remove] can not be called in a Filter");
        }

        @Override
        public void putAll(Map<? extends String, ? extends E> m) {
            throw new IllegalArgumentException("[putAll] can not be called in a Filter");
        }

        @Override
        public void clear() {
        }

        @Override
        public Set<String> keySet() {
            return Collections.singleton(fieldName);
        }

        @Override
        public Collection<E> values() {
            return Collections.singleton(value);
        }

        @Override
        public Set<Entry<String, E>> entrySet() {
            return Collections.singleton(new Entry<String, E>() {

                @Override
                public String getKey() {
                    return fieldName;
                }

                @Override
                public E getValue() {
                    return value;
                }

                @Override
                public E setValue(E value) {
                    return Filter.this.value;
                }
            });
        }

        static String operatorFilterToString(final String fieldName, final String operator, final Object value) {
            return "Operator Filter{"
                    + "fieldName='" + fieldName + '\''
                    + ", operator='" + operator + '\''
                    + ", value=" + value
                    + '}';
        }
    }
}